<?php
/**
* Implements all the Atom functions.
*<p>An Atom is the basic element that compose a page. Atoms are cached toghether for fast search and
*manipulation. AtomData, instead, is stored in individual files for memory efficiency</p>
*<p>Atoms do have a certain set of attributes such as: <ul>
*<li><b>id</b>: Unique identifier used to address an atom.</li>
*<li><b>tag_cloud</b>: array of tags to categorize the atom (only for human classification).</li>
*<li><b>leaves</b>: list of all the "children" Atoms linked to the atom.</li>
*<li><b>parents</b>: List of all the "parents" Stoms that this atom links to.</li>
*<li><b>type</b>: Data type contained in the Atom. This is actually a <code>type = get_class(AtomData)</code></li>
*</ul>
*<p>In order to support future expansion and keep backward compatibility, all the attributes are stored ad
*a named array. This allow for adding new attributes later on and keeping full compatibility with Atoms created 
*before. Just keep in mind that reading any unset attribute will return false (and setting it will actually create
* it in the Atom instance)</p>
*<p>Working with leaves and parents requires a bit of effort to keep track of the complete Atoms tree. Any Atom can
*have an unlimited number of children leaves, as well can be linked to an un limited number or parent Atoms. This
*is to reduce the number or atoms: it is always advisable to reuse atoms and not create two atoms with identical
*content.</p>
*<p>Each Atom instance is contained inside the AtomCache class, and serialized to disk in one file. To keep this
*file size small and scalable, all data relative to the Atom is stored in a separate file (see <i>AtomData</i> class
* for more informations). The Atom gives you two methods to <i>StoreData</i> and <i>LoadData</i>, which will manage
* formatting for the data files and do some run-time checks to ensure that what we are loading is actually valid.
*Care is taken to ensure that no duplication occurs between AtomData and Atom: the only attribute which is 
*duplicated is the atom's ID so that is things goes wrong you do not end up with a useless folder filled with
*meaningless AtomsData files. The <i>title</i> attribute is stored inside the AtomData: this requires sometime
* to load the AtomData file only for that, but keeps the AtomCache file a lot smaller.</p>
*<p>Each Atom can be categorized by one or more (or none) "tags", all toghether the tags are called the <b>
*tag_cloud</b> of the Atom. Tags only purpose is to sort atoms and make things easyer for humans whi have to
* shuffle between a LOT of atoms. They have no purpose whatsoever as far as the actual engine is concerned.</p>
*<p>The type attribute is guessed by get_class on the AtomData, and is cached in the Atom class to avoid having
*to load the AtomData only to check the type. Otherwise, when filtering Atoms for type the Engine would have to
*load all the AtomData and will not scale properly.</p>
*@package Atoms
*@author Willy Gardiol <willy@gardiol.org>
*@version 1.0
*@todo Add a database backend so that Atoms and AtomsData can be stored not only on filesystem.
*/
class Atom extends AtomBase
{	# Local storage to keep AtomData classes cached at runtime.
	static private $data = array();
	/**
	* Build the full filename to the AtomData
	*@return string
	*/
	private function Filename()
	{
		return "site_data/data/".$this->Get("id");
	}
	/**
	* Return the list of parent Atoms (only atom id)
	*@return array array of atom_ids
	*/
	public function GetParents()
	{
		return array_keys( $this->Get("parents") );
	}
	/**
	* Add a new parent for the Atom. Its like being adopted.
	* Since we can be adopted more than once by the same father (yes, strange but true) we keep track of
	* this by using a an integer counter based on the parent atom id
	*@param string $atom_id The ID of the new parent
	*@param bool $delete If true, the parent will be removed
	*/
	private function SetParent( $atom_id, $delete = false )
	{
		$parents = $this->Get("parents");
		# In case of delete, decrease the atom count number
		if ( isset( $parents[$atom_id] ) && $delete )
			$parents[$atom_id]--;
		# in case the atom already exists increase the count number
		else if ( isset( $parents[$atom_id] ) )
			$parents[$atom_id]++;
		# otherwise initialize it to 1
		else
			$parents[$atom_id] = 1;
		# Clear the count number if its <= 0
		if ( isset( $parents[$atom_id] ) && ($parents[$atom_id] <= 0) )
			unset( $parents[$atom_id] );
		$this->Set("parents", $parents );
	}
	/**
	* Build the new Atom and set all default values.
	*@param array $tags Array of tags
	*@param AtomData $data Data to be bind to the Atom
	*/
	public function __construct( $tags, $data )
	{
		$this->Set( "id", uniqid("DT") );
		Atom::$data[$this->Filename()] = $data;
		$this->Set("tag_cloud", $tags);
		# Leaves are categorized by a leaf_id: we can have the same atom in as many leaves as we want!
		$this->Set("leaves", array());
		# Parents are tracked only by count, we do not track which leaf we are for each father
		$this->Set("parents", array());
		$this->Set("type", get_class( $data ));

		AtomCache::RegisterAtom( $this );
		$data->Initialize( $this->Get("id") );
	}
	/**
	* Load the AtomData from disk (with some integrity checks).
	*@return AtomData
	*/
	public function LoadData()
	{
		$filename = $this->Filename();
		if ( isset( Atom::$data[$filename] ) )
			return Atom::$data[$filename];
		else if ( file_exists( $filename ) && 
				(($store = unserialize(file_get_contents($filename)) ) !== false))
			return Atom::$data[$filename] = $store["data"];
		return false;
	}
	/**
	* Store the AtomData to disk (AtomData is cached, so no need to pass it as a parameter).
	*/
	public function StoreData()
	{
		$filename = $this->Filename();
		if ( isset( Atom::$data[$filename] ) )
		{
			$this->Set("type", get_class( Atom::$data[$filename] ));
			$store = array("data" => Atom::$data[$filename] );
			return file_put_contents( $filename, serialize($store) );
		}
		return false;
	}
	/**
	* We do not delete Atoms: we trash them. Atoms here are moved to the trashcan.
	*@param bool $force is set to true, trash the atom even if it has leaves
	*@return bool
	*/
	public function MoveToTrash( $force = false )
	{
		$old_filename = $this->Filename();
		$new_filename = "site_data/data/trash/".$this->Get("id");
		# Remove pointers to loaded data if any
		unset( Atom::$data[$old_filename] );
		# Move the data file first
		if ( rename( $old_filename, $new_filename ) )
		{
			$id = $this->Get("id");
			$leaves = $this->Get("leaves");
			$parents = $this->Get("parents");
			# Do not delete us if we have leaves, and FORCE is not set
			if ( (count( $leaves ) > 0) && !$force )
				return false;
			# First, free all of our leaves: remove us as one of their parents
			foreach ( $leaves as $leaf_id => $child_id )
			{
				$atom = AtomCache::GetAtom( $child_id );
				$atom->SetParent( $id, true );
				unset( $leaves[ $leaf_id ] );
			}
			# Then, tell to each one of our parents that we are no more
			foreach ( $parents as $parent_id => $count )
			{
				$atom = AtomCache::GetAtom( $parent_id );
				foreach ( $parent_leaves = $atom->Get("leaves") as $key => $leave )
					# we must iterate on every child of the father to remove us from any branch 
					if ( $leave == $id ) 
						unset( $parent_leaves[$key] );
				$atom->Set("leaves", $parent_leaves);
				unset( $parents[ $parent_id ] );
			}
			return true;
		}
		return false;
	}
	/**
	* Leaves are stored by keys, so this will return the leaf with given key
	*@param string $leaf_id the key of the leav to return
	*@return Atom
	*/
	public function  GetLeaf( $leaf_id )
	{
		$leaves = $this->Get("leaves");
		return isset( $leaves[$leaf_id] ) ? $leaves[$leaf_id] : false;
	}
	/**
	* Set a new leaf (or replace existing). A key is required to identify the leaf.
	*All the proper check to ensure that the old leaf (if any) is freed and the new leaf
	*is properly attached (parent/leaf properties...) are done.
	*@param string $leaf_id A key to identify the leaf
	*@param string $atom_id the Atom id for the new leaf
	*@return string The atom id just added (of false if any error).
	*/
	public function SetLeaf( $leaf_id, $atom_id )
	{
		$id = $this->Get("id");
		$leaves = $this->Get("leaves");
		$set = isset( $leaves[$leaf_id] );
 
		# Leaf is already set AND is equal to the new one, do not do anything
		if ( $set && ( $leaves[$leaf_id] == $atom_id ) )
			return $atom_id;
		if ( $set )
		{
			if ( !$this->UnsetLeaf( $leaf_id ) )
				return false;
			else // Reload leaves
				$leaves = $this->Get("leaves");
		}
		$my_tags = $this->GetTagCloud();
		$atom = AtomCache::GetAtom( $atom_id );
		# Set the new leaf or skip if none to be set
		if ( $atom !== false )
		{	# Add my tags to the new leaf
			$atom->AddTags( $my_tags );
			# Set myself as new parent for the new leaf
			$atom->SetParent( $id );
		}
		# Save new leaf
		$leaves[ $leaf_id ] = $atom_id;
		# Store updated leaves
		$this->Set("leaves", $leaves);
		return $atom_id;
	}
	/**
	* Unsetting a  leaf: delete a leaf.
	*All the proper check to ensure that the old leaf exists.
	*@param string $leaf_id A key to identify the leaf
	*@return bool status.
	*/
	public function UnsetLeaf( $leaf_id  )
	{
		$id = $this->Get("id");
		$leaves = $this->Get("leaves");
		if ( isset( $leaves[$leaf_id] ) ) # Leaf is set, we must free the old one
		{
			$my_tags = $this->GetTagCloud();
 			$atom = AtomCache::GetAtom( $leaves[$leaf_id] );
			if ( $atom !== false )
			{
				# Remove my tags from the old leaf, free it!
				$atom->RemoveTags( $my_tags );
				# Remove myself as parent of the old leaf!
				$atom->SetParent( $id, true );
			}
			# Free the leaf!
			unset( $leaves[$leaf_id] );
			# Store updated leaves
			$this->Set("leaves", $leaves);
			return true;
		}
		return false;
	}
	/**
	* Get all the leaves of the Atom in a nice array (only atom id are returned).
	*@return array List of atom ids
	*/
	public function GetLeaves()
	{
		return $this->Get("leaves");
	}
	/**
	* Add new tags to the atom.
	*@param array $new_tags new tags to add (do not need to be unique among the already existing tags)
	*/
	public function AddTags( $new_tags )
	{
		$this->Set("tag_cloud", array_unique(array_merge( $this->Get("tag_cloud"),$new_tags)));
	}
	/**
	* Remove some tags from the current tag_cloud.
	*@param array $tags  tags to delete
	*/
	public function RemoveTags( $tags )
	{
		$ret = false;
		$old_tags = $this->Get("tag_cloud");
		foreach ( $old_tags as $k => $t )
			if ( in_array( $t, $tags ) )
				unset( $old_tags[$k] );
		$this->Set("tag_cloud", $old_tags);
	}
	/**
	*Return all the tags.
	*@return array List of strings
	*/
	public function GetTagCloud()
	{
		return $this->Get("tag_cloud");
	}
	/**
	* Force set of a new tag_cloud. This will substitute the entire tag_cloud with a new one.
	*@param array $tags Array of strings. Will NOT be added to the current tags, will overwrite current tags.
	*/
	public function SetTagCloud( $tags )
	{
		$this->Set("tag_cloud", array_unique($tags));
		return true; // useful.. please leave this here
	}
	/**
	* Return atom id.
	*@return string
	*/
	public function GetId()
	{
		return $this->Get("id");
	}
	/**
	* Return atom type.
	*@return string
	*/
	public function GetAtomType()
	{
		$type = $this->Get("type");
		// TODO: remove this, will be obsolete very soon
		if ( $type === false )
		{
			$data = $this->LoadData();
			$type = get_class( $data );
			$this->Set("type", $type );
		}
		return $type;
	}
}
?>